/**************************************************************/
/*         p2p: adapter for peer-to-peer GUI connection       */
/*                      by H.G. Muller                        */
/*                                                            */
/* This adapter can be run as a WB engine on two different    */
/* machines to provide a connection between them.             */
/**************************************************************/

#define VERSION "0.5"

#define BUFLEN   8000
#define MAXMOVES 1000
#define MOVELEN  29

#define DEFAULT_PASSWORD "Have fun, have WinBoard!"
#define DEFAULT_MESSAGE  "Welcome to p2p " VERSION
#define DEFAULT_PORT     "27015"

#ifdef WIN32
#    undef UNICODE

#    define WIN32_LEAN_AND_MEAN

#    include <windows.h>
#    include <winsock2.h>
#    include <ws2tcpip.h>
#else
#    include <sys/types.h>
#    include <sys/socket.h>
#    include <string.h>
#    include <netinet/in.h>
#    include <signal.h>
#    include <errno.h>
#    include <pthread.h>
#    include <netdb.h>

#    define INVALID_SOCKET (-1)
#    define SOCKET_ERROR (-1)
#    define closesocket close
typedef int SOCKET;
typedef struct sockaddr SOCKADDR;
typedef unsigned int uint32_t;
int WSAGetLastError() { return errno; }
void WSACleanup() { return; }
#endif

#include <stdlib.h>
#include <stdio.h>


char *errors[] = {
  "OK",
  "WSAStartup failed (error %ld)\n",
  "getaddrinfo failed (error %ld)\n",
  "socket failed (error %ld)\n",
  "Unable to connect to server!\n",
  "send failed (error %ld)\n",
  "shutdown failed (error %ld)\n",
  "Server port already occupied (error %ld)\n",
  "listen failed (error %ld)\n",
  "accept failed (error %ld)\n",
  "recv failed (error %ld)\n",
  "shutdown failed (error %ld)\n",
  "Unable to resolve host name\n"
};

typedef void Proc(void);

void CleanUp (SOCKET s)
{
  // cleanup
  closesocket(s);
  WSACleanup();
}

char errmess[100];

int Error (int major, long int minor, SOCKET s)
{

  sprintf(errmess, errors[major], minor);
  if(s != INVALID_SOCKET) closesocket(s);
  return 1;
}


int InitNetwork () 
{
#if WIN32
  int res;
  static WSADATA wsaData;

  // Initialize Winsock
  res = WSAStartup(MAKEWORD(2,2), &wsaData);
  if (res) return Error(1, res, INVALID_SOCKET);
#endif
  return 0;
}

int MakeConnection (char *host, int port, SOCKET *s) 
{   // returns 0 on success, with socket in s that is TCP-connected to host
  SOCKET ConnectSocket = INVALID_SOCKET;
  struct sockaddr_in sa;
  struct hostent *he;
  struct in_addr **list;

  // Create a SOCKET for connecting to server
  ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (ConnectSocket == INVALID_SOCKET) return Error(3, WSAGetLastError(), INVALID_SOCKET);

  if(!(he = gethostbyname(host))) return Error(3, 0, INVALID_SOCKET);
  list = (struct in_addr **) he->h_addr_list;

  sa.sin_family = AF_INET;
  sa.sin_addr = *list[0]; //sa.sin_addr.s_addr = inet_addr(host);
  sa.sin_port = htons(port);

  // Connect to server.
  if (connect( ConnectSocket, (SOCKADDR *) &sa, sizeof(sa)) == SOCKET_ERROR) return Error(4, 0, INVALID_SOCKET);

  *s = ConnectSocket;
  return 0; // success
}

int SetupServer (char *port, SOCKET *s)
{
  SOCKET ListenSocket = INVALID_SOCKET;
  struct sockaddr_in sa;

  // Create a SOCKET for connecting to server
  ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (ListenSocket == INVALID_SOCKET) return Error(3, WSAGetLastError(), INVALID_SOCKET);

  // Setup the TCP listening socket
  sa.sin_family = AF_INET;
  sa.sin_addr.s_addr = 0;
  sa.sin_port = htons(atoi(port));
  if (bind( ListenSocket, (SOCKADDR *) &sa, sizeof(sa)) == SOCKET_ERROR) return Error(7, WSAGetLastError(), ListenSocket);

  *s = ListenSocket;
  return 0;
}

int WaitForCaller (SOCKET ListenSocket, SOCKET *s)
{
  SOCKET ClientSocket = INVALID_SOCKET;

  printf("# listening\n"), fflush(stdout);
  if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) return Error(8, WSAGetLastError(), ListenSocket);
  printf("# done listening\n"), fflush(stdout);

  // Accept a client socket
  *s = ClientSocket = accept(ListenSocket, NULL, NULL);
  if (ClientSocket == INVALID_SOCKET) return Error(9, WSAGetLastError(), ListenSocket);
  printf("# accepted incoming call\n"), fflush(stdout);

  return 0; // success
}

#define NONE  0
#define WHITE 1
#define BLACK 2
#define COLOR (BLACK|WHITE)

#define OFF  0
#define ON   1

#define CONNECTED     1
#define DISCONNECTED  0

// global state variables shared between threads

int state, started, listening, ping, myCnt, hisCnt, engineSide, hisSide, computer;
int hisMps, hisTc, hisInc, mps, timeControl, inc;
int timeLeft, otimLeft;
char myVariant[20], hisVariant[20];
char myMoves[MAXMOVES][MOVELEN+1], hisMoves[MAXMOVES][MOVELEN+1];
char startPos[BUFLEN], hisPos[BUFLEN];
char hostName[256];
char password[80]   = DEFAULT_PASSWORD;
char portNumber[80] = DEFAULT_PORT;
char serverPort[80] = DEFAULT_PORT;
char welcome[1000]  = DEFAULT_MESSAGE;
int lim = BUFLEN;
char chatMessage[1000];
SOCKET clientSocket; // to share with GUILoop

int ReadFromPeer (SOCKET s, char *buf)
{
  int res;
  char *p, *q, *r;
  static char rcvBuf[BUFLEN+1];
  static int cnt;
  while((p = strchr(rcvBuf, '\n')) == NULL) {
    if(state == DISCONNECTED || cnt >= BUFLEN) return 0; // treat buffer overflow as EOF
    res = recv(s, rcvBuf+cnt, BUFLEN-cnt, 0);
    if(!res) printf("telluser connection closed by peer\n");
    if(res < 0) {
      printf("telluser recv failed with error: %d\n", WSAGetLastError()), fflush(stdout);
      res = 0;
    }
    if(!res) return 0;
    cnt += res;
  }
  rcvBuf[cnt] = 0; p++; r = buf;
  for(q = rcvBuf; q < p; ) *r++ = *q++; *r = 0;
  for(q = rcvBuf; p < rcvBuf+cnt; ) *q++ = *p++; *q = 0;
  cnt = q - rcvBuf;
  strcpy(buf+lim-1, "\n"); // clip input line length to user limit
  printf("# web->p2p: %s", buf);
  return 1;
}

void SendToPeer (char *command, char *args)
{
  char buf[BUFLEN];
  int res;

  if(state == DISCONNECTED) return;
  snprintf(buf, BUFLEN, command, args);
  printf("# p2p->web: %s", buf);
  res = send( clientSocket, buf, strlen(buf), 0 );
  if (res == SOCKET_ERROR) {
    Error(5, WSAGetLastError(), clientSocket);
    clientSocket = INVALID_SOCKET;
    printf("telluser %s", errmess);
  }
}

void SendTC ()
{
  char buf[80];
  snprintf(buf, 80, "level %d %d %d\n", mps, timeControl, inc);
  SendToPeer("%s", buf); // also TC
}

void SendMoveToGUI (char *move)
{
  char *p=move, *q;
  strcpy(myMoves[myCnt++], p); // log move
  SendToPeer("move %s", p);    // keep peer in sync
  while((q = strchr(p, ',')))  // WB-Alien multi-leg move; send leading legs
    *q = 0, printf("move %s,\n", p), p = q+1;
  printf("move %s", p);        // send the move (or last leg of it)
}

void PeerLoop (SOCKET s)
{
  int i = -1;
  char inBuf[BUFLEN], command[80];

  clientSocket = s; // so GUILoop can use it
  hisCnt = hisVariant[0] = 0;
  if(ping >= 0) {
    printf("pong %d\n", ping); // reply to backlogged ping
    ping = -1;
  }

  while(1) {
    fflush(stdout);
    if(!ReadFromPeer(s, inBuf)) break; // EOF from socket
    if(hisCnt >= MAXMOVES) break;

    // extract the first word
    sscanf(inBuf, "%79s", command);

    // recognize the command, and execute it
    if(!strcmp(command, "quit"))    { started = 0; printf("1/2-1/2 {Disconnect}\n"); break; } // abort any game, terminate loop
    if(!strcmp(command, "move")) {
      strcpy(inBuf+MOVELEN+4, "\n"); strcpy(hisMoves[hisCnt++], inBuf+5); // store it (after clipping for safety)
      printf("# started=%d my=%d his=%d engine=%d\n", started, myCnt, hisCnt, engineSide);
      if(started > 1 && hisCnt == myCnt+1 && (myCnt&1)+1 == engineSide) SendMoveToGUI(inBuf+5); // move our GUI is waiting for; play it
      continue;
    }
    if(!strcmp(command, "go")) { // he started a game
      int bad=0;  // check if he started in a compatible way; if not, consider it a rejection of our game offer
      printf("# started=%d my=%d his=%d\n", started, myCnt, hisCnt);
      hisSide = 2 - (hisCnt & 1); // if he has done no moves, he wants black (2), with 1 move he wants white (1)
      if(started) { // we already started, and now opponent started too
        if(hisSide != engineSide) bad = 1; // he must play the side we proxy for
        else if(strcmp(myVariant, hisVariant)) bad = 1; // must play same variant
        else if(hisTc != timeControl || hisInc != inc || hisMps != mps) bad = 3; // must play same TC
        else if(myCnt - hisCnt > 1 || hisCnt - myCnt > 1) bad = 2; // one must have submitted an extra move to start with
        else if(strcmp(startPos, hisPos)) bad = 4; // must play same variant
        else for(i=0; i<myCnt && i<hisCnt; i++) if(strcmp(myMoves[i], hisMoves[i])) { bad = 2; break; } // forced moves must be the same
        if(bad) { // incompatable; we must change Terminate game towards our GUI so he can accept counter proposal
          printf("# rejected start (%d) %s/%s i=%d\n", bad, myVariant, hisVariant, i);
        } else {
          started = 2; SendToPeer("accepted\n", "");
          if(hisCnt > myCnt) SendMoveToGUI(hisMoves[hisCnt-1]); // play the move he is ahead
        }
      }
      // if we did not start yet, we ignore this, let his offer stand, and wait for our GUI to start as well
      if(!started || bad) {
        // print his (counter-)proposal
        snprintf(command, 80, "%ds%c%d", hisTc, hisMps > 0 ? '/' : hisInc ? '+' : 0, hisMps > 0 ? hisMps : hisInc);
        if(hisMps < 0) snprintf(command, 80, "%ds/move", hisTc);
        if(started) // abort the game our GUI started
          printf("1/2-1/2 {please start a %s %s game as %s}\n", command, hisVariant, hisSide == WHITE ? "black" : "white");
        if(!computer) printf("telluser Your opponent wants to play %s %s as %s. "
            "Please start such a game, or make a counter proposal.%s\n",
            command, hisVariant, hisSide == BLACK ? "black" : "white", bad == 3 ? " (match TC!)" : "");
      }
      continue;
    }
    if(!strcmp(command, "result"))  { if(started) printf(inBuf[7] == '*' ? "1/2-1/2 {adjourn}\n" : "%s", inBuf+7), started = 0; continue; }
    if(!strcmp(command, "name"))    { inBuf[strlen(inBuf)-1] = 0; printf("feature myname=\"%s\"\n", inBuf+5); continue; }
    if(!strcmp(command, "text"))    { printf("1 0 0 0 %s", inBuf+5); continue; }
    if(!strcmp(command, "variant")) { sscanf(inBuf+8, "%19s", hisVariant); continue; }
    if(!strcmp(command, "accepted")){ started = 2; if(hisCnt > myCnt) SendMoveToGUI(hisMoves[hisCnt-1]); continue; }
    if(!strcmp(command, "setboard")){ strcpy(hisPos, inBuf+9); hisCnt = !strstr(hisPos, " w "); continue; }
    if(!strcmp(command, "remove"))  { // peer did takeback. Request our GUI to do the same
      hisCnt -= 2; started = 0; printf("telluser Please take back two ply, using Truncate Game and restarting\n");
      continue;
    }
    if(!strcmp(command, "new"))     { if(started > 1) started = 0; hisVariant[0] = hisCnt = hisMoves[0][0] = hisPos[0] = 0; continue; }
    if(!strcmp(command, "hallo"))   { printf("telluser %s says: %s", hostName, inBuf+5); continue; }
    if(!strcmp(command, "undo"))    { hisCnt--; continue; }
    if(!strcmp(command, "level"))   { sscanf(inBuf+6, "%d %d %d", &hisMps, &hisTc, &hisInc); continue; }
    if(!strcmp(command, "draw"))    { printf("offer draw\n"); continue; }
    if(!strcmp(command, ""))    { continue; }
  }
}

void StartPeerThread (Proc proc)
{
#ifdef WIN32
  DWORD thread_id;
  CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) proc, (LPVOID) NULL, 0, &thread_id);
#else
  pthread_t t;
  signal(SIGINT, SIG_IGN);
  pthread_create(&t, NULL, (void*(*)(void*)) proc, NULL);
#endif
}

void PeerProc ()
{
  SOCKET socket;

  if(MakeConnection(hostName, atoi(portNumber), &socket)) printf("telluser (%s) %s", hostName, errmess), fflush(stdout);
  else {
    state = CONNECTED;
    clientSocket = socket;
    if(password[0]) {
      char inBuf[BUFLEN];
      SendToPeer("%s\n", password);
      while(ReadFromPeer(socket, inBuf)) { // this should be "Password required", "Password accepted" or "Wait for matching password"
        if(!strncmp(inBuf, "Password ", 9)) break;
        if(!strncmp(inBuf, "Wait ", 5)) printf("telluser Server is waiting for arrival of opponent with same password\n"), fflush(stdout);
        else printf("# server said '%s' while waiting for password confirmation\n", inBuf);
      }
    }
    PeerLoop(socket);
    // make sure connection is closed before exiting thread
    closesocket(socket);
    state = DISCONNECTED;
  }
}

void ListenProc ()
{
  SOCKET socket, ListenSocket;
  char inBuf[BUFLEN+1], pw[BUFLEN+1];
  int res;

  if(SetupServer(serverPort, &ListenSocket)) { printf("telluser %s", errmess); return; }
  while(1) {
    res = WaitForCaller(ListenSocket, &socket);

    if(!res) { // connection made from outside
      if(state == CONNECTED) {
        char *buf = "hallo Sorry, peer is already involved in a game\n";
        send( socket, buf, strlen(buf), 0 );
      } else {
        do {
          clientSocket = socket;
          state = CONNECTED;
          if(password[0]) { // check password
            SendToPeer("Password required\n", "");
            if(!ReadFromPeer(socket, inBuf)) break;
            if(!sscanf(inBuf, "%[^\n]", pw)) break;
            if(strcmp(pw, password)) break;
          }
          SendToPeer("hallo %s\n", welcome);
          if(ping) printf("pong %d\n", ping); // release GUI from suspension
          PeerLoop(socket);
        } while(0);
      }
      state = DISCONNECTED;
      send( socket, "quit\n", 5, 0 );
      closesocket(socket);
    }
  }

  // No longer need server socket
  closesocket(ListenSocket);
}

void GUIProc ()
{
  int i;
  char inBuf[BUFLEN+1], command[80];

  while(1) { // infinite loop


    fflush(stdout);                 // make sure everything is printed before we do something that might take time
    // wait for input, and read it until we have collected a complete line
    for(i = 0; (inBuf[i] = getchar()) != '\n'; i++);
    inBuf[i+1] = 0;

    // extract the first word
    sscanf(inBuf, "%s", command);
    printf("# GUI->p2p: %s\n", command);

    // recognize the command, and execute it
    if(!strcmp(command, "quit"))    { SendToPeer("quit\n", ""); break; } // breaks out of infinite loop
    if(!strcmp(command, "force"))   { engineSide = NONE; started = 0; continue; }
    if(!strcmp(command, "otim"))    { sscanf(inBuf, "time %d", &otimLeft); SendToPeer("%s", inBuf); continue; }
    if(!strcmp(command, "time"))    { sscanf(inBuf, "time %d", &timeLeft); SendToPeer("%s", inBuf); continue; }
    if(!strcmp(command, "level"))   {
      int min, sec=0;
      sscanf(inBuf, "level %d %d %d", &mps, &min, &inc) == 3 ||  // if this does not work, it must be min:sec format
        sscanf(inBuf, "level %d %d:%d %d", &mps, &min, &sec, &inc);
      timeControl = 60*min + sec;
      SendTC(); // inform peer
      continue;
    }
    if(!strcmp(command, "protover")){
      printf("feature ping=1 setboard=1 colors=0 usermove=1 debug=1 sigint=0 sigterm=0\n");
      printf("feature variants=\""
          "normal,fischerandom,seirawan,gothic,capablanca,caparandom,janus,"
          "spartan,knightmate,super,cylinder,fairy,grand,"
          "suicide,losers,giveaway,atomic,crazyhouse,3check,twokings,"
          "shatranj,courier,makruk,custom,"
          "multi,checkers,10x10+0_checkers,amazons,go,9x9+0_go,13x13+0_go,19x19+0_go,"
          "xiangqi,shogi,5x5+5_shogi,chu,dai,12x12+0_fairy,dada,maka,tai,tenjiku"
          "\"\n"); // an awful amount of variants. WB protocol should implement a wildcard here?
      printf("feature option=\"GUI line-length limit -spin %d 80 8000\"\n", lim); // GUI protection
      printf("feature name=1 myname=\"P2P " VERSION "\"\n");
      printf("feature option=\"Welcome message: -string %s\"\n", welcome);// printed when someone connects to server
      printf("feature option=\"Password: -string %s\"\n", password);      // password (sent and accepted)
      printf("feature option=\"IP address -string %s\"\n", hostName);     // to connect to
      printf("feature option=\"Remote port -string %s\"\n", portNumber);  // to connect to
      printf("feature option=\"Chat line: -string \"\n");   // line to send
      printf("feature option=\"Send line -save\"\n");       // send chat line
      printf("feature option=\"Connect -save\"\n");         // connect to IP address
      printf("feature option=\"Disconnect -button\"\n");    // disconnect
      printf("feature done=1\n");
      continue;
    }
    if(!strcmp(command, "option")) { // setting of engine-define option; find out which
      char c;
      if(sscanf(inBuf+7, "GUI line-length limit=%d", &lim) == 1) continue;
      if(sscanf(inBuf+7, "Welcome message:=%[^\n]", welcome) == 1) continue;
      if(sscanf(inBuf+7, "Password:=%[^\n]", password)     == 1) continue;
      if(sscanf(inBuf+7, "Password:=%c", &c) == 1 && c == '\n') { *password = 0; continue; } // the above does not read empty...
      if(sscanf(inBuf+7, "Chat line:=%[^\n]", chatMessage) == 1) continue;
      if(sscanf(inBuf+7, "IP address=%[^\n]", hostName)    == 1) continue;
      if(sscanf(inBuf+7, "Remote port=%s", portNumber)     == 1) continue;
      if(!strcmp(inBuf+7, "Send line\n")) {
        if(chatMessage[0]) SendToPeer("text %s\n", chatMessage), printf("1 0 0 0 > %s\n", chatMessage);
        else printf("askuser text Type message for peer:\n");
        continue;
      }
      if(!strcmp(inBuf+7, "Connect\n")) {
        if(state == DISCONNECTED) StartPeerThread(PeerProc); else printf("telluser You are already connected!\n");
        continue;
      }
      if(!strcmp(inBuf+7, "Disconnect\n")) {
        if(state == CONNECTED) SendToPeer("quit\n", ""), state = DISCONNECTED, closesocket(clientSocket);
        else printf("telluser You were not connected!\n");
        continue;
      }
      continue;
    }
    if(!strcmp(command, "new"))     {
      engineSide = BLACK;
      computer= started = myCnt = myVariant[0] = startPos[0] = myMoves[0][0] = 0;
      SendToPeer("new\n", "");
      continue;
    }
    if(!strcmp(command, "variant")) {
      FILE *f; char buf[100];
      sscanf(inBuf+8, "%s", myVariant); SendToPeer(inBuf, "");
      snprintf(buf, 100, "%s.dat", myVariant);
      if(f = fopen(buf, "r")) {
        int c;
        while((c = fgetc(f)) != EOF) printf("%c", c);
      }
      continue;
    }
    if(!strcmp(command, "sd"))      { continue; }
    if(!strcmp(command, "st"))      { sscanf(inBuf, "st %d", &timeControl); mps = -1; inc = 0; SendTC(); continue; }
    if(!strcmp(command, "ping"))    { if(state == DISCONNECTED) ping = atoi(inBuf+5); else ping = -1, printf("po%s", inBuf+2); continue; }
    //  if(!strcmp(command, ""))        { sscanf(inBuf, " %d", &); continue; }
    if(!strcmp(command, "setboard")){
      strcpy(startPos, inBuf+9); myCnt = !strstr(startPos, " w "); // leave first move empty if black to move
      engineSide = NONE; SendToPeer("%s", inBuf);
      continue;
    }
    if(!strcmp(command, "undo"))    { myCnt--; SendToPeer("undo\n", ""); started = 0; continue; }
    if(!strcmp(command, "remove"))  { myCnt -= 2; SendToPeer("%s", inBuf); continue; }
    if(!strcmp(command, "go")) {
      SendToPeer("variant %s\n", myVariant); SendTC(); // send our game parameters in case GUI set them when not yet connected
      engineSide = (myCnt&1) + 1; started = 1; SendToPeer("go\n", "");
      continue;
    }
    if(!strcmp(command, "computer")){ computer = 1; continue; }
    if(!strcmp(command, "name"))    { SendToPeer("%s", inBuf); continue; }
    if(!strcmp(command, "text"))    { SendToPeer("text %s", inBuf+5); printf("1 0 0 0 > %s", inBuf+5); continue; }
    if(!strcmp(command, "result"))  { if(started > 1) SendToPeer("%s", inBuf), started = 0; continue; }
    if(!strcmp(command, "draw"))    { SendToPeer("draw\n", ""); continue; }
    // ignored commands (only meaningful for true engines):
    if(!strcmp(command, "post"))    { continue; }
    if(!strcmp(command, "nopost"))  { continue; }
    if(!strcmp(command, "random"))  { continue; }
    if(!strcmp(command, "easy"))    { continue; }
    if(!strcmp(command, "hard"))    { continue; }
    if(!strcmp(command, "xboard"))  { continue; }
    if(!strcmp(command, "ics"))     { continue; }
    if(!strcmp(command, "accepted")){ continue; }
    if(!strcmp(command, "rejected")){ continue; }
    if(!strcmp(command, ""))  {  continue; }
    if(!strcmp(command, "usermove")){
      strcpy(myMoves[myCnt++], inBuf+9); // store it
      SendToPeer("move %s", inBuf+9);    // and keep peer in sync
      if(myCnt == 1 && !started && engineSide == BLACK) {
        SendToPeer("variant %s\n", myVariant); SendTC(); // send our game parameters in case GUI set them when not yet connected
        started = 1, SendToPeer("go\n", ""); // implied go when user on this side plays white and started with move
      }
      continue;
    }
    printf("Error: unknown command\n");
  }
}

int main(int argc, char **argv)
{

  if(InitNetwork()) { printf("tellusererror Network initialization error: %s", errmess); return 0; }

  if(argc > 2 && !strcmp(argv[1], "-p")) strcpy(serverPort, argv[2]), argc--, argv++;

  // create separate thread for handling input from peer
  if(argc > 1) { // if so requested on command line, make active connection
    strcpy(hostName, argv[1]);
    if(argc > 2) strcpy(password, argv[2]);
    StartPeerThread(PeerProc);
  } else StartPeerThread(ListenProc); // otherwise start listening passively


  // handle GUI->engine traffic in original thread
  GUIProc();
  CleanUp(clientSocket);

  return 0;
}
